package cn.lijiajia3515.cairo.security.web.authentication;

import cn.lijiajia3515.cairo.core.business.AccountBusiness;
import cn.lijiajia3515.cairo.core.business.AuthBusiness;
import cn.lijiajia3515.cairo.core.business.Business;
import cn.lijiajia3515.cairo.core.business.DefaultBusiness;
import cn.lijiajia3515.cairo.core.business.HttpStatusBusiness;
import cn.lijiajia3515.cairo.core.result.error.ErrorBusinessResult;
import cn.lijiajia3515.cairo.http.converter.AbstractHttpMessageHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.security.web.authentication.www.NonceExpiredException;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 空认证处理 异常处理 (spring)
 * <p>未通过异常验证处理 </p>
 * <p>异常message 输出到 response</p>
 *
 * <p>AuthenticationException : 认证异常的父类，抽象类</p>
 * <p>BadCredentialsException : 登录凭证（密码）异常</p>
 * <p>InsufficientAuthenticationException : 登陆凭证不够充分而抛出的异常</p>
 * <p>SessionAuthenticationException : 会话并发管理时抛出的异常，例如会话总数超出最大限制数</p>
 * <p>UsernameNotFoundException : 用户名不存在异常</p>
 * <p>PreAuthenticatedCredentialsNotFoundException : 身份预认证失败异常</p>
 * <p>ProviderNotFoundException : 未配置AuthenticationProvider异常</p>
 * <p>AuthenticationServiceException : 由于系统问题而无法处理认证请求异常</p>
 * <p>InternalAuthenticationServiceException : 由于系统问题而无法处理认证请求异常，和AuthenticationServiceException不同之处在于如果外部系统出错，不会抛出该异常</p>
 * <p>AuthenticationCredentialsNotFoundException : SecurityContext中不存在认证主体时抛出的异常</p>
 * <p>NonceExpiredException : HTTP摘要认证时随机数过期异常</p>
 * <p>RememberMeAuthenticationException : RememberMe认证异常</p>
 * <p>CookieTheftException : RememberMe认证时Cookie被盗窃异常</p>
 * <p>InvalidCookieException :RememberMe认证时无效的Cookie异常</p>
 * <p>AccountStatusException :账户状态异常</p>
 * <p>LockedException :账户被锁定异常</p>
 * <p>DisabledException :账户被禁用异常</p>
 * <p>CredentialsExpiredException :登录凭证（密码）过期异常</p>
 * <p>AccountExpiredException :账户过期异常</p>
 *
 *
 * <p>1.2、AccessDeniedException(权限异常)</p>
 * <p>AccessDeniedException ：权限异常的父类</p>
 * <p>AuthorizationServiceException : 由于系统问题而无法处理权限时抛出异常</p>
 * <p>CsrfException : Csrf令牌异常</p>
 * <p>MissingCsrfTokenException : Csrf令牌缺失异常</p>
 * <p>InvalidCsrfTokenException : Csrf令牌无效异常</p>
 */
@Slf4j
public class CairoHttpMessageAuthenticationEntryPoint extends AbstractHttpMessageHandler implements AuthenticationEntryPoint {
	protected static final ThrowableAnalyzer analyzer = new DefaultThrowableAnalyzer();

	protected Business business = DefaultBusiness.Unknown;
	protected HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

	@Getter
	@Setter
	@Accessors(fluent = true)
	protected String realmName = "Cairo";


	public CairoHttpMessageAuthenticationEntryPoint(List<HttpMessageConverter<?>> converters) {
		super(converters, null);
	}

	public CairoHttpMessageAuthenticationEntryPoint(List<HttpMessageConverter<?>> converters, ContentNegotiationManager manager) {
		super(converters, manager);
	}

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		log.debug("authentication failed ", exception);
		init();
		// 处理header信息
		header(request, response, exception);

		// 处理business
		business(request, response, exception);
		// 设置响应状态码
		response.setStatus(httpStatus.value());
		// 输出payload message
		write(business, exception, request, response);
	}

	protected void business(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws HttpMediaTypeNotAcceptableException, IOException {
		if (exception instanceof AccountStatusException) {
			if (exception instanceof AccountExpiredException) {
				business = AccountBusiness.Expired;
			} else if (exception instanceof DisabledException) {
				business = AccountBusiness.Disabled;
			} else if (exception instanceof LockedException) {
				business = AccountBusiness.Locked;
			} else if ((exception instanceof CredentialsExpiredException)) {
				business = AuthBusiness.Expired;
			}
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Expired;
		}
		// 认证缺失
		if (exception instanceof AuthenticationCredentialsNotFoundException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Required;
		}


		// session 伪造 或者 解析失败
		if (exception instanceof RememberMeAuthenticationException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Bad;
		}

		// 凭证错误
		if (exception instanceof BadCredentialsException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Bad;
		}

		// security user detail service 用户名不存在
		if (exception instanceof UsernameNotFoundException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Bad;
		}

		// security 前置验证 异常
		if (exception instanceof PreAuthenticatedCredentialsNotFoundException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.Bad;
		}

		// HTTP摘要认证时随机数过期异常
		if (exception instanceof NonceExpiredException) {
			httpStatus = HttpStatus.UNAUTHORIZED;
			business = AuthBusiness.NonceExpired;
		}

		// security - 服务不可用
		if (exception instanceof AuthenticationServiceException ||
			exception instanceof ProviderNotFoundException ||
			exception instanceof InsufficientAuthenticationException) {
			httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
			business = HttpStatusBusiness.ServiceUnavailable;
		}


		// TODO SessionAuthenticationException 允许用户同时在线 异常

	}

	public void init() {
		business = DefaultBusiness.Unknown;
		httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
	}

	public void write(Business business, AuthenticationException exception, HttpServletRequest request, HttpServletResponse response) throws HttpMediaTypeNotAcceptableException, IOException {
		ErrorBusinessResult returnValue = ErrorBusinessResult.builder()
			.business(business)
			.exceptions(Arrays.asList(analyzer.determineCauseChain(exception))).build();
		writeWithMessageConverters(returnValue, new ServletServerHttpRequest(request), new ServletServerHttpResponse(response));
	}


	public void header(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
		Map<String, String> parameters = new LinkedHashMap<>();
		if (this.realmName != null) {
			parameters.put("realm", this.realmName);
		}
		String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
		response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
	}

	public static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
		StringBuilder wwwAuthenticate = new StringBuilder();
		wwwAuthenticate.append("??? ");
		if (!parameters.isEmpty()) {
			wwwAuthenticate.append(" ");
			int i = 0;
			for (Map.Entry<String, String> entry : parameters.entrySet()) {
				wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
				if (i != parameters.size() - 1) {
					wwwAuthenticate.append(", ");
				}
				i++;
			}
		}
		return wwwAuthenticate.toString();
	}
}
